diff options
Diffstat (limited to 'app/[lng]')
6 files changed, 333 insertions, 27 deletions
diff --git a/app/[lng]/evcp/(evcp)/items/page.tsx b/app/[lng]/evcp/(evcp)/items/page.tsx index 144689ff..31dcaf11 100644 --- a/app/[lng]/evcp/(evcp)/items/page.tsx +++ b/app/[lng]/evcp/(evcp)/items/page.tsx @@ -1,7 +1,7 @@ +// app/items/page.tsx (업데이트) import * as React from "react" import { type SearchParams } from "@/types/table" -import { getValidFilters } from "@/lib/data-table" import { Skeleton } from "@/components/ui/skeleton" import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" import { Shell } from "@/components/shell" @@ -9,7 +9,6 @@ import { searchParamsCache } from "@/lib/items/validations" import { getItems } from "@/lib/items/service" import { ItemsTable } from "@/lib/items/table/items-table" - interface IndexPageProps { searchParams: Promise<SearchParams> } @@ -17,16 +16,21 @@ interface IndexPageProps { export default async function IndexPage(props: IndexPageProps) { const searchParams = await props.searchParams const search = searchParamsCache.parse(searchParams) + + // pageSize 기반으로 모드 자동 결정 + const isInfiniteMode = search.perPage >= 1_000_000 - const validFilters = getValidFilters(search.filters) - - const promises = Promise.all([ - getItems({ - ...search, - filters: validFilters, - }), + console.log('Page searchParams:', searchParams) + console.log('Parsed search:', search) + console.log('isInfiniteMode (pageSize >= 1M):', isInfiniteMode) - ]) + // 페이지네이션 모드일 때만 서버에서 데이터 가져오기 + // 무한 스크롤 모드에서는 클라이언트에서 SWR로 데이터 로드 + const promises = isInfiniteMode + ? null + : Promise.all([ + getItems(search), // searchParamsCache의 결과를 그대로 사용 + ]) return ( <Shell className="gap-2"> @@ -37,25 +41,21 @@ export default async function IndexPage(props: IndexPageProps) { Package Items </h2> <p className="text-muted-foreground"> - Item을 등록하고 관리할 수 있습니다.{" "} - {/* <span className="inline-flex items-center whitespace-nowrap"> - <Ellipsis className="size-3" /> - <span className="ml-1">버튼</span> - </span> - 을 통해 담당자 연락처, 입찰 이력, 계약 이력, 패키지 내용 등을 확인 할 수 있습니다. */} + {/* Item을 등록하고 관리할 수 있습니다. */} + {isInfiniteMode && ( + <span className="ml-2 text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded"> + 무한 스크롤 모드 + </span> + )} </p> </div> </div> </div> <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> - {/* <DateRangePicker - triggerSize="sm" - triggerClassName="ml-auto w-56 sm:w-60" - align="end" - shallow={false} - /> */} + {/* DateRangePicker 등 추가 컴포넌트 */} </React.Suspense> + <React.Suspense fallback={ <DataTableSkeleton @@ -67,8 +67,9 @@ export default async function IndexPage(props: IndexPageProps) { /> } > + {/* 통합된 ItemsTable 컴포넌트 사용 */} <ItemsTable promises={promises} /> </React.Suspense> </Shell> ) -} +}
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/rfq-tech/[id]/layout.tsx b/app/[lng]/evcp/(evcp)/rfq-tech/[id]/layout.tsx index 35f76a76..0bb62fe0 100644 --- a/app/[lng]/evcp/(evcp)/rfq-tech/[id]/layout.tsx +++ b/app/[lng]/evcp/(evcp)/rfq-tech/[id]/layout.tsx @@ -33,15 +33,15 @@ export default async function RfqLayout({ const sidebarNavItems = [ { title: "Matched Vendors", - href: `/${lng}/evcp/rfq/${id}`, + href: `/${lng}/evcp/rfq-tech/${id}`, }, { title: "TBE", - href: `/${lng}/evcp/rfq/${id}/tbe`, + href: `/${lng}/evcp/rfq-tech/${id}/tbe`, }, { title: "CBE", - href: `/${lng}/evcp/rfq/${id}/cbe`, + href: `/${lng}/evcp/rfq-tech/${id}/cbe`, }, ] diff --git a/app/[lng]/partners/(partners)/cbe-tech/page.tsx b/app/[lng]/partners/(partners)/cbe-tech/page.tsx new file mode 100644 index 00000000..94e3825d --- /dev/null +++ b/app/[lng]/partners/(partners)/cbe-tech/page.tsx @@ -0,0 +1,86 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getCBEbyVendorId, } from "@/lib/rfqs-tech/service" +import { searchParamsCBECache } from "@/lib/rfqs-tech/validations" +import { getServerSession } from "next-auth" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { TbeVendorTable } from "@/lib/tech-vendor-rfq-response/vendor-tbe-table/tbe-table" +import * as React from "react" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { CbeVendorTable } from "@/lib/tech-vendor-rfq-response/vendor-cbe-table/cbe-table" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise<SearchParams> +} + +export default async function CBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsCBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const session = await getServerSession(authOptions) + const vendorId = session?.user.companyId + // const vendorId = "17" + + const idAsNumber = Number(vendorId) + + const promises = Promise.all([ + getCBEbyVendorId({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + + return ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + Commercial Bid Evaluation + </h2> + <p className="text-sm text-muted-foreground"> + CBE에 응답하고 커뮤니케이션을 할 수 있습니다.{" "} + </p> + </div> + </div> + </div> + + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + {/* <DateRangePicker + triggerSize="sm" + triggerClassName="ml-auto w-56 sm:w-60" + align="end" + shallow={false} + /> */} + </React.Suspense> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <CbeVendorTable promises={promises} /> + </React.Suspense> + </Shell> + ) +} diff --git a/app/[lng]/partners/(partners)/rfq-tech/page.tsx b/app/[lng]/partners/(partners)/rfq-tech/page.tsx new file mode 100644 index 00000000..e3e5895a --- /dev/null +++ b/app/[lng]/partners/(partners)/rfq-tech/page.tsx @@ -0,0 +1,133 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { searchParamsRfqsForVendorsCache } from "@/lib/rfqs-tech/validations" +import { RfqsVendorTable } from "@/lib/tech-vendor-rfq-response/vendor-rfq-table/rfqs-table" +import { getServerSession } from "next-auth" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import Link from "next/link" +import { Button } from "@/components/ui/button" +import { LogIn } from "lucide-react" +import { getRfqResponsesForVendor } from "@/lib/tech-vendor-rfq-response/service" + +interface IndexPageProps { + searchParams: Promise<SearchParams> +} + +export default async function IndexPage(props: IndexPageProps) { + const searchParams = await props.searchParams + const search = searchParamsRfqsForVendorsCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // Get session + const session = await getServerSession(authOptions) + + // Check if user is logged in + if (!session || !session.user) { + // Return login required UI instead of redirecting + return ( + <Shell className="gap-6"> + <div className="flex items-center justify-between"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + RFQ + </h2> + <p className="text-muted-foreground"> + RFQ를 응답하고 커뮤니케이션을 할 수 있습니다. + </p> + </div> + </div> + + <div className="flex flex-col items-center justify-center py-12 text-center"> + <div className="rounded-lg border border-dashed p-10 shadow-sm"> + <h3 className="mb-2 text-xl font-semibold">로그인이 필요합니다</h3> + <p className="mb-6 text-muted-foreground"> + RFQ를 확인하려면 먼저 로그인하세요. + </p> + <Button size="lg" asChild> + <Link href="/partners"> + <LogIn className="mr-2 h-4 w-4" /> + 로그인하기 + </Link> + </Button> + </div> + </div> + </Shell> + ) + } + + // User is logged in, proceed with vendor ID + const vendorId = session.user.companyId + + // Validate vendorId (should be a number) + const idAsNumber = Number(vendorId) + + if (isNaN(idAsNumber)) { + // Handle invalid vendor ID (this shouldn't happen if authentication is working properly) + return ( + <Shell className="gap-6"> + <div className="flex items-center justify-between"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + RFQ + </h2> + </div> + </div> + <div className="flex flex-col items-center justify-center py-12 text-center"> + <div className="rounded-lg border border-dashed p-10 shadow-sm"> + <h3 className="mb-2 text-xl font-semibold">계정 오류</h3> + <p className="mb-6 text-muted-foreground"> + 업체 정보가 올바르게 설정되지 않았습니다. 관리자에게 문의하세요. + </p> + </div> + </div> + </Shell> + ) + } + + // If we got here, we have a valid vendor ID + const promises = Promise.all([ + getRfqResponsesForVendor({ + ...search, + filters: validFilters, + }, idAsNumber) + ]) + + return ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + RFQ + </h2> + <p className="text-muted-foreground"> + RFQ를 응답하고 커뮤니케이션을 할 수 있습니다. + </p> + </div> + </div> + </div> + + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + {/* DateRangePicker can go here */} + </React.Suspense> + + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <RfqsVendorTable promises={promises} /> + </React.Suspense> + </Shell> + ) +}
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/tbe-tech/page.tsx b/app/[lng]/partners/(partners)/tbe-tech/page.tsx new file mode 100644 index 00000000..69cf3902 --- /dev/null +++ b/app/[lng]/partners/(partners)/tbe-tech/page.tsx @@ -0,0 +1,85 @@ +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { getTBEforVendor } from "@/lib/rfqs-tech/service" +import { searchParamsTBECache } from "@/lib/rfqs-tech/validations" +import { getServerSession } from "next-auth" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { TbeVendorTable } from "@/lib/tech-vendor-rfq-response/vendor-tbe-table/tbe-table" +import * as React from "react" +import { Skeleton } from "@/components/ui/skeleton" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" + +interface IndexPageProps { + // Next.js 13 App Router에서 기본으로 주어지는 객체들 + params: { + lng: string + id: string + } + searchParams: Promise<SearchParams> +} + +export default async function RfqTBEPage(props: IndexPageProps) { + const resolvedParams = await props.params + const lng = resolvedParams.lng + + // 2) SearchParams 파싱 (Zod) + // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 + const searchParams = await props.searchParams + const search = searchParamsTBECache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + const session = await getServerSession(authOptions) + const vendorId = session?.user.companyId + // const vendorId = "17" + + const idAsNumber = Number(vendorId) + + const promises = Promise.all([ + getTBEforVendor({ + ...search, + filters: validFilters, + }, + idAsNumber) + ]) + + + return ( + <Shell className="gap-2"> + <div className="flex items-center justify-between space-y-2"> + <div className="flex items-center justify-between space-y-2"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + Technical Bid Evaluation + </h2> + <p className="text-sm text-muted-foreground"> + TBE에 응답하고 커뮤니케이션을 할 수 있습니다.{" "} + </p> + </div> + </div> + </div> + + <React.Suspense fallback={<Skeleton className="h-7 w-52" />}> + {/* <DateRangePicker + triggerSize="sm" + triggerClassName="ml-auto w-56 sm:w-60" + align="end" + shallow={false} + /> */} + </React.Suspense> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + } + > + <TbeVendorTable promises={promises} /> + </React.Suspense> + </Shell> + ) +} diff --git a/app/[lng]/partners/(partners)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx b/app/[lng]/partners/(partners)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx index 9a305318..f69aa525 100644 --- a/app/[lng]/partners/(partners)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx +++ b/app/[lng]/partners/(partners)/vendor-data/form/[packageId]/[formId]/[projectId]/[contractId]/page.tsx @@ -48,7 +48,7 @@ export default async function FormPage({ params, searchParams }: IndexPageProps) } // 5) DB 조회 - const { columns, data } = await getFormData(formCode, packageIdAsNumber); + const { columns, data, editableFieldsMap } = await getFormData(formCode, packageIdAsNumber); // 6) formId 및 report temp file 조회 @@ -71,6 +71,7 @@ export default async function FormPage({ params, searchParams }: IndexPageProps) columnsJSON={columns} dataJSON={data} projectId={Number(projectId)} + editableFieldsMap={editableFieldsMap} // 새로 추가 mode={mode} // 모드 전달 /> </div> |
